Итак, сегодня мы будем расширять функциональность Круптара. Многим уже полюбился этот чрезвычайно гибкий инструмент для выдирания, редактирования и вставки обратно игрового скрипта. И сегодня, дорогой друг, ты поймёшь, что его возможности действительно безграничны, ибо он позволяет редактировать и упакованный текст! Конечно, для этого потребуется чуть больше, чем базовые знания в ромхакинге, но ведь когда-то нужно развиваться. Но обо всём по порядку.
Для хорошего усвоения нового урока нам понадобятся:
Исходники плагинов под него (нас будет интересовать Null плагин)
РОМ-жертва: Snake's Revenge (U).nes
Вскрытие пациента.
Для начала нужно поближе познакомиться с нашим сегодняшним гостем: Snake's Revenge. Про игру много говорить не буду (многие её не жалуют), зато в желающих перевести её дефицита нет (насколько я знаю, игра до сих пор полностью не переведена). На самом деле, основной скрипт здесь не зажат, зато текст, появляющийся в TRCVR (прадедушка Codec’а), не хочет отыскиваться Relative Search’ем. Поэтому после непродолжительных манипуляций (содержание которых выходит за рамки данного документа) находим, наконец, вот такой кусок кода, отвечающий за распаковку текста в RAM, откуда в дальнейшем он перемещается в память PPU (кто привык копаться в голых данных эмпирическими методам и методами научного тыка могут сразу переходить к концу данной главы):
|
ROM:A75A uncomp: ; CODE XREF:
ROM:A7BEj ROM:A75A LDA byte_41 ;загружаем номер сообщения ROM:A75C ASL A ;умножаем на два (указатели
двухбайтовые) ROM:A75D ROM:A75E LDA PTR_table,Y ROM:A761 STA Ptr_low ROM:A763 LDA PTR_table+1,Y ROM:A766 STA Ptr_high ;Загружаем соответствующий указатель ROM:A768 LDA byte_42 ;Загружаем смещение от начала
сообщения ROM:A76A BNE loc_A76F ;В начале здесь всегда ноль. ROM:A76C STA Offset ROM:A76F ROM:A76F loc_A76F: ; CODE XREF: ROM:A76Aj ROM:A76F LDY Offset ROM:A772 LDX #0 ROM:A774 LDA (Ptr_low),Y ;Загружаем байт из упакованного потока ROM:A776 ROM:A776 loc_A776: ; CODE XREF: ROM:loc_A7B1j ROM:A776 LSR A ROM:A777 LSR A ROM:A778 STA Unpacked_char_Buffer,X;
сохранение в поток распакованного ROM:A77B INX ; текста ROM:A77C CPX #$40 ; '@' ;в сообщении не больше $40 символов ROM:A77E BEQ loc_A784 ROM:A780 CMP #$3F ; '?' ; $3F- конец распаковки ROM:A782 BNE Index_Operation ROM:A784 ROM:A784 loc_A784: ; CODE XREF: ROM:A77Ej ROM:A784 STY Offset ROM:A787 JMP loc_A7C0 ROM:A78A ;
--------------------------------------------------------------------------- ROM:A78A ROM:A78A Index_Operation: ; CODE XREF: ROM:A782j ROM:A78A TXA ROM:A78B LSR A ; Хитрая процедура, чтобы в случае,
если из ROM:A78B ; предыдущего
байта в последующий перекидывается 6 ROM:A78B ; бит, да еще от двух младших избавляемя, чтобы этот ROM:A78B ; последующий байт не
пропал - в итоге при X=3 ROM:A78B
; Y не
изменится ROM:A78C LSR A ROM:A78D STA byte_7 ROM:A78F TXA ROM:A790 CLC ROM:A791 SBC byte_7 ROM:A793 CLC ROM:A794 ADC Offset ROM:A797 ROM:A798 TXA ROM:A799 AND #3 ROM:A79B STA Counter ROM:A79D LDA (Ptr_low),Y ;Загружаем байт из упакованного потока ROM:A79F STA First_Byte ROM:A7A1 INY ROM:A7A2 LDA (Ptr_low),Y
;Загружаем байт из упакованного потока ROM:A7A4 ROM:A7A4 TwoBits_InSecondByte: ; CODE XREF:
ROM:A7AEj ROM:A7A4 DEC Counter ROM:A7A6 BMI loc_A7B1 ROM:A7A8 LSR First_Byte ; Два младших бита первого байта
переходят ROM:A7A8 ; в два старших второго, причем два
младших ROM:A7A8 ; бита второго уже не используются ROM:A7AA ROR A ROM:A7AB LSR First_Byte ROM:A7AD ROR A ROM:A7AE
JMP TwoBits_InSecondByte ROM:A7B1 ;
--------------------------------------------------------------------------- ROM:A7B1 ROM:A7B1 loc_A7B1: ; CODE XREF: ROM:A7A6j ROM:A7B1 JMP loc_A776 ;
дальнейшая обработка распакованного текста |
Испугался? Думаю, в графическом представлении все выглядит не так страшно:

Если кто ещё не догадался, то это банальная шестибитная кодировка одного символа. То есть в индексе каждого символа используется не 8 бит (целый байт), а всего 6 (два старших не используются). Легко подсчитать, что двоичное 00111111 – даёт нам максимально возможных 64 используемых символа (а нам больше и не надо). Таким образом, имеем входной битовый поток, в котором, например, в первом байте будут 6 бит первого символа и ещё два бита от второго символа, во втором байте – уже четыре бита второго символа и четыре бита третьего символа и так далее. Разумеется, relative search не мог обнаружить текст, упакованный таким образом.
По старинке.
Ну, как водится, можно было бы написать простенькую программку, которая бы читала входной поток и выдавала в выходной индексы символов (практически готовый скрипт, только его ещё надо прогнать через таблицу). Для подтверждения эмпирических данных, полученных из анализа кода игры. Так и сделаем:
|
Sinput:=Tmemorystream.Create; //Входной
файл Soutput := Tmemorystream.Create; //Выходной файл Sinput.Position := Offset; //Оffset
– смещение начала упакованного сообщения Sinput.LoadFromFile(Filename); Bit :=
0;//Временная переменная, которая будет носить в себе один бит SI := 0;//Счетчик бит входного
потока DI :=
0;//Счетчик бит выходного потока DB := 0;// Destination_Byte - Байт выходного потока count := 0;// Счетчик сколько нам нужно распаковать байт Sinput.Read(SB,1);// Source_Byte - SB – байт входного потока While Count < $30 do //для
контроля распакуем 48 символов Begin Bit := (SB and $80) shr 7;//берём старший бит входного байта SB := SB shl 1; inc (SI); If SI = 8 Then Begin //Если байт входного потока
исчерпан, читаем новый SI :=0; SInput.Read(SB,1); end; DB := DB shl 1; DB:= DB or
Bit; //Записываем наш бит в
младший бит выходного потока inc (DI); If DI = 6
then Begin //Если байт выходного потока
исчерпан, сохраняем его в выходной поток. DI:=0; SOutput.Write(DB,1); DB :=0; inc (Count); end; end; SOutput.SaveToFile('Out.bin'); Sinput.free; Soutput.Free; |
Думаю, излишне говорить, что распакованный файл по своему содержимому должен совпадать с текстовой частью тайловой карты, когда в неё выводится игровой текст, которую можно увидеть в Name Table Viewer в FCEUXD, плюс байты конца строки и конца сообщения (вот заодно и узнаем их значения и сможем окончательно составить таблицу).
Кстати о значениях байтов: что ещё мы можем узнать из кода игры? Ну, начнём со стандартной процедуры: составление таблицы (мою можно найти в проекте Круптара, распаковав его как .zip архив). Обратили внимание на строки:
3B=
3C="
Откуда я узнал? Да вот отсюда:
|
ROM:A7C5 LDA Unpacked_char_Buffer,Y ROM:A7C8 CMP #$3B ; ';' ROM:A7CA BEQ loc_A7FD ;
процедура подмены индекса на пробел ROM:A7CC
CMP #$3C ; '<' ROM:A7CE BNE loc_A7FF ROM:A7D0 LDA #$75 ; в Pattern Table это индекс значка
" ROM:A7D2 BNE loc_A7FF |
Строки типа:
3D
3E
ends
специфичны для Круптара и означают, что $3D и $3E – байты разрыва строки, а $3F – байт конца строки.
И, наконец, финальный аккорд: когда я осматривал таблицу указателей (она есть в коде – я её переименовал в PTR_table), то обнаружил, что она расположена прямо перед блоками с текстом, но между старшим байтом последнего указателя и местом, на которое указывает первый указатель, стоит один непонятный байт! Можете убедиться сами, открыв РОМ по смещению 0хEA88 – перед ним стоит указатель на последний текстовый блок (имеет значение $B0A3, затем идёт неизвестный байт $03, затем (по смещению 0xEA89) первый байт упакованного потока текста, на который ссылается первый указатель таблицы со значением $AA79). Экспериментально распаковав несколько байт второго текстового блока своей тестовой программкой, и дойдя до этого текста в игре (это брифинг пилота о том, что нужно проникнуть в логово врага и собрать информацию – сразу после прохождения первого экрана), поменяем этот байт, скажем, на ноль, единичку и т.п. и выясним, что этот байт отвечает за то, кто именно говорит в этот момент по TRCVR (волосатый мужик, негр, девушка, пилот…). Назовём его байтом атрибута текста. С ним придётся считаться: скорее всего, он расположен перед каждым текстовым блоком (я уже показал, что он есть перед первым и вторым), поэтому, когда мы станем работать с полным скриптом игры, нам придётся вынимать и вставлять обратно этот байт атрибута неупакованным (небольшое затруднение, с которым Круптар с честью справится). Даже, несмотря на то, что указатель-то ссылается как раз на текстовый блок за байтом атрибута, а не на сам атрибут:
|
$A645:B9 3E
AA LDA $AA3E,Y @ $AA40 = #$B4; Загрузка младшего байта
указателя на второй текстовый блок. (в $AA3E таблица указателей) $A648:38
SEC $A649:E9 01 SBC #$01 ;Отнимаем
единичку! Теперь будет загружаться байт, стоящий перед вторым текстовым
блоком $A64B:85 10
STA $0010 = #$00 $A64D:B9 $A650:E9 00
SBC #$00 $A652:85 11
STA $0011 = #$A2 $A654:A0 00
LDY #$00 $A656:B1 10 LDA ($10),Y @ $AAB3
= #$03 ;Непосредственно загрузка байта атрибута. |
Великолепно! Мы узнали всё, что нам нужно и ассемблерная часть на этом закончена.
Что теперь? Ну, в былые времена, я бы посоветовал вам прикрутить к тестовой программке поддержку таблиц (что само по себе уже нетривиальная задача), затем поддержку использования и изменения указателей, сохранять всё это в текстовый файл и молиться каждый раз как вставляете текст, чтобы получившийся упакованный скрипт не налез на какие-нибудь жизненно важные для игры данные или делать проверку на размер получившегося выходного потока, что также не прибавляет программе легковесности.
Но мы живём в новой эре, дружище. Поэтому для всех этих рутинных процедур можно использовать Круптар, а получившийся проект будет являться компактным файликом, который содержит скрипт правильно отформатированный по переносам строк, с возможностью поиска и тому подобными милыми вещами. Более того, поддержка пойнтеров добавится почти автоматически! А работать переводчику в таком проекте просто одно удовольствие: не укладываешься в рамки отведённого пространства – Круптар деликатно напомнит об этом. В общем, ромхакер делает то, что ему действительно нравится – остаётся один на один только с процедурой упаковки/распаковки данных – все побочные телодвижения (типа вынимания скрипта, работы с таблицами и пойнтерами) производятся в Круптаре за пару минут.
Новая эра.
Итак, приступим к делу: наша задача написать плагин к Круптару, который распаковывал бы входной поток, представлял всё это в понятном для Круптаре виде, а затем, после перевода делал бы всё наоборот.
Несколько слов о плагинах: на самом деле исходный текст плагина – это простой проект в Дельфи с расширением .dpr и после компиляции будет представлять собой динамическую библиотеку, вроде тех, что использует система (.dll), только с расширением .kpl. С нашими исходными файлами можно работать как с простым проектом Дельфи, то есть существует возможность запускать приложение для отладки (только для этого необходимо указать Круптар, как Host приложение (Run ->Parameters… ->Local ->Host Application)). Компилировать же плагин можно и без Круптара. Стоит также отметить, что Круптар всегда использует плагины. И даже, когда мы вытаскиваем неупакованный скрипт, в Круптар загружен плагин «Standard.kpl».
Писать свой плагин, разумеется, мы будем не с чистого листа. Печка, от которой будем плясать – «Null», то есть пустой плагин (плагин для извлечения строк стандартного формата PChar - то есть с ноликом в конце строки). Итак, откроем Null.dpr и внимательно изучим. Сразу переименуем его (мой называется SR.dpr) Попутно можно будет поменять имя и описание плагина (KPLDescription) с Null на что-нибудь более содержательное (этот текст будет отображаться в окне Справка -> Библиотеки). Пока что нас интересует только распаковка (о запаковке можно позаботиться потом – пока что просто достанем наш скрипт в читаемом виде).
Распаковка.
После подключения к проекту Круптара нашего плагина весь входной поток скрипта будет проходить через функцию «GetStrings», за один проход которой будет обработано одно сообщение (или в общем случае, блок, на который ссылается указатель, введённый в проект Круптара):
|
Function GetStrings(X, Sz: Integer): PTextStrings; stdcall; // X - Смещение текстовых данных в
роме
// Sz - размер строки,
указанный в ptStringLength (его можно
//не
использовать в своих плагинах) Var P: PChar; // null-terminated string begin Result :=
NIL; If (X >= RomSize) or (X < 0) then
Exit; // RomSize – размер загруженного в проект
РОМа New(Result, Init); //Интциализация With Result^.Add^ do begin P := Addr(ROM^[X]); //
ROM: Pbytes – типизированный указатель на данные РОМа Str := ''; While P^ <>
#0 do //Если встретили ноль, значит строка закончена begin Str
:= Str + P^; //Копируем данные в строку Inc(P); end; end; end; |
Тут ничего не скажешь: Djinn в первую очередь заботился об эффективности и быстродействии, а не о наглядности кода. Хотя, вся нужная нам информация может быть найдена в исходниках плагина (в т.ч. см. Needs.pas), всё же поясним тип возвращаемого функцией результата – PtextStrings:
|
PTextStrings = ^TTextStrings; // PTextStrings - это динамический массив
состоящий из PTextString TTextStrings = Object Root, Cur: PTextString;// Root - указатель на первый элемент массива Count: Integer; // количество элементов в массиве Constructor Init; //подробно
см. Needs.pas Function Add: PTextString; Function
Get(I: Integer): PTextString; Destructor
Done; end; |
где PTextString:
|
PTextString = ^TTextString; TTextString = Record Str: String; Next: PTextString; end; |
В функции GetStrings и будет
происходить наша распаковка. Вот один из вариантов реализации этого (код
учебный, поэтому в нём даже есть лишние переменные, добавленные для наглядности):
|
Function GetStrings(X, Sz: Integer): PTextStrings; stdcall; Var B,SI,DI,DB,Bit: byte; //Переменные, как и в моей
тестовой программе begin Result :=
NIL; Bit:=0; SI:=0; DI:=0; DB :=0; If (X >= RomSize) or (X < 0) then Exit; New(Result, Init); With Result^.Add^ do begin //Легко узнать в дальнейших
строках мою тестовую программу Str:=''; Str := Str
+ Char(ROM^[X]); // первый байт не распаковываем - атрибут. inc (x); B :=
ROM^[X]; Repeat //бесконечный цикл, выход из которого
возможен только по Break Bit := (B and $80) shr 7; //берём старший бит входного байта B := B shl 1; inc (SI); If SI = 8 Then Begin //Если байт входного потока
исчерпан, читаем новый SI :=0; inc(X); B :=
ROM^[X]; end; DB := DB shl 1; DB:= DB or
Bit; //Записываем наш бит в младший бит выходного потока inc (DI); If DI = 6
then Begin //Если байт выходного потока
исчерпан, сохраняем его в выходной поток. DI:=0; if DB =
$3F then begin
//Если распакован символ конца сообщения, сохраняем его в выходной поток и
выходим. Str := Str +
Char(DB); break ; end else Str := Str + Char(DB); DB :=0; end; Until False; end; end; |
Компилируем наш .dpr c изменённой функцией GetStrings и получаем файл с расширением .kpl, который нужно поместить в папку \Lib, расположенную в корневом каталоге Круптара.
Проект.
Пришло время создать непосредственно проект, с которым сможет работать переводчик. Открываем Круптар, жмём Ctrl+N и в первую очередь вводим имена оригинального и переведённого РОМов (разумеется, пока что это два одинаковых файла). В моём случае это соответственно ‘Snake's Revenge (U).nes’ и ‘Snake's Revenge (Rus).nes’ (на случай, если вы будете открывать мой проект). Далее жмём Ctrl+T и добавляем таблицу из файла (или сразу жмём Ctrl+Alt+T), хотя можно составить таблицу прямо в проекте. Убедитесь, что Круптар правильно воспринял байты окончания и разрыва строк и не выдал предупреждения по загруженной таблице. Теперь жмём Ctrl+G и добавляем группу. Тут же указываем в графе grLibrary наш плагин из выпадающего списка (мой называется SR.kpl). grIsDictionary оставляем false. Далее добавляем блоки для текста – это место в РОМе, которое Круптар будет использовать для вставки обратно переведённого скрипта. Ну, начало-то мы знаем где: исходя из значения первого указателя в таблице и учтя, что перед первым текстовым блоком есть ещё один байт атрибута, начало будет по смещению 0хEA88. А конец где? Пока что это не так важно – пока что мы и не вставляем-то толком скрипт. Укажем это место наобум и можно с запасом: 0xFFFF. Затем выбираем пойнтеры, и «добавить элемент». Здесь опций уже больше.
В итоге у вас должно получиться что-то вроде этого:

Здесь я менял только поля используемых таблиц, ptPointeSize и ptReference. Далее я расскажу, что означает каждая графа, а те, кому не интересны все возможности Круптара, могут смело переходить к чтению последнего абзаца этой части.
- ptInput/OutputTable – это таблицы, по которым будет производиться, соответственно, вынимание и вставка скрипта (в моём случае, эти таблицы одинаковые, так как я еще не перерисовывал шрифт и не знаю кодов будущих русских букв).
- ptDirctionary указывается группа-словарь, это применяется при работе DTE/MTE скриптами. Группа становится словарём, если grIsDictionary = TRUE, и становятся активны опции:
Параметры словаря;
Генерировать словарь;
Словарь в таблицу;
Опция "Генерировать словарь" берёт все списки, в которых указан словарь в ptDictionary и генерирует по тексту словарь, все слова заносятся в эту группу по порядку. Но работает эта опция не очень эффективно.
Борьба с DTE/MTE при помощи Круптара выходит за рамки этого документа, но те, кто заинтересовался этой темой, могут ознакомиться с проектом под Beetlejuice на NES, где для основного скрипта применяется MTE.
- ptPointerSize – размер указателей в байтах (на NES двухбайтовые указатели). Здесь может быть и ноль. Например, в некоторых играх указателей на отдельные строки нет вообще (какие адреса писать при добавлении блока пойнтеров см. объяснение ptStringLength далее).
- ptReference – очень важный параметр, связанный с особенностью работы Круптара. Объясню подробнее: как только вы укажете Круптару смещение указателя, он считает его в соответствии с заданным форматом (ptPointerSize, ptMotorola и т.п.), затем прибавит к значению данного указателя величину ptReference и получит смещение первого байта входного текстового блока. Как видите, в моём случае, это h400F. Как уже много раз было сказано, первый байт упакованного текстового блока расположен по смещению 0хEA89, а значение первого указателя: $AA79 (первый указатель расположен по смещению 0xEA4E). Таким образом, hEA89-hAA79 = h4010. Не забудем про байт атрибута: ведь указатель ссылается на текстовый блок, а не на него: придётся отнять ещё единичку, чтобы наш атрибут вошёл в скрипт. Итого получаем h400F. Проверяем: если мы скормим Круптару указатель по смещению 0xEA4E, он прочитает его как величину $AA79, затем прибавит к ней полученное только что нами h400F и начнёт читать из РОМа байт по смещению 0хEA88 – то, что надо, это же наш первый байт атрибута ($03)!
- ptInterval – промежуток в байтах между отдельными указателями (у нас указатели хранятся в монолитном едином блоке).
- ptShiftLeft - [исходный пойнтер] shl [значение в этом поле]. Часто формат указателей в игре такой, что для их использования приходится производить сдвиг байта влево, причем иногда не один раз.
- ptMultiply – очень часто при вставке скрипта обратно необходимо, чтобы начало строк было кратно по адресу какому-либо числу, которое и указывается в этом поле (например, на GBA некоторые инструкции правильно считывают данные только кратные четырём: ldr r0,[r1] – здесь адрес в r1 должен быть кратен 4 обязательно). Это связано с особенностями железа. И если предположить, что у нас GBA проект и наша строка может начаться, скажем, с адреса: 0х1АB4A1, то Круптар забивает 1AB4A1,1AB4A2,1AB4A3 нулями, а потом вставляет наш текст в 1AB4A4, соответственно корректируя указатели на эту строку.
- ptStringLength – указывается длина в байтах каждой строки. Фиксированная длина строки встречается во многих играх. Если ptPointerSize равен нулю, а ptStringLength не равен нулю, то при добавлении блока пойнтеров нужно вписывать адреса первой и последней строк. Если же ptPointerSize и ptStringLength будут равны нулю, то вписываем адреса первого и последнего байтов текстового блока (внутри блока Круптар будет ориентироваться по знаку окончания строки). И, как обычно, если ptPointerSize не равен нулю, то вписываем адреса первого и последнего указателей.
- ptMotorola – способ хранения указателей: в случае NES младший байт указателя хранится перед старшим, в процессорах Motorola – наоборот.
- ptSigned – указатели, которые могут иметь положительное и отрицательное значение (т.е. адресуют вперёд и назад от своей позиции) иногда применяются в играх на SEGA. Как правило, в РОМе стоит блок с текстом, а после него пойнтер, указывающий назад на нужную строку (скажем, имеем указатель со значением «-34»; Круптар берет адрес пойнтера, потом читает его значение, отсчитывает 34 байта назад и считывает эту строку). Если указатели двухбайтные, то они могут принимать значения от -32768 до +32767. Указатели, имеющие положительные значение, естественно, адресуют вперёд. Ещё одна особенность ptSigned: ptReference в этом случае уже можно не вписывать, так как адресация будет происходить от смещения в РОМе указателя.
ptPunType - такой тип используется в игре The Punisher (NES), от того и название "PunType". А смысл здесь в том, что указатель сам по себе разделён некоторым количеством байт, которое в свою очередь указывается в ptInterval. Скажем, это мне очень пригодилось при переводе BEE-52, где почти все указатели хранятся в операндах ассемблерных команд:
…
LDX #$B4; младший байт указателя
LDY #$82; старший байт указателя
….
Если кто не знаком с ассемблером 6502, то в хексредакторе мы этот код увидим в виде:
$A2 $B4 $A0 $82 – как видим, между старшим и младшим байтами указателя стоит не интересующий нас байт. Вот тут мы выставляем в поле ptPunType true, а в графе ptInterval пишем 2. Почему не 1? Это дань совместимости проекта с предыдущими версиями Круптара. В случае если ptPunType = true, в графу ptInterval вписывается «значение = количество_байт_в_разрыве +1». Подчеркну, что это не распространяется на случай, когда у нас есть интервал между самими указателями (prPunType = false).
- ptAutoStart – если значение этого поля true, то значение поля ptReference становится равным смещению в РОМе первого указателя в таблице (ptReference уже не заполняем), и к значению каждого указателя в этой таблице прибавляется ptReference, чтобы получить адрес строки (значение каждого указателя в этом случае есть смещение от начала таблицы до первого байта строки, на которую ссылается этот указатель).
- ptPtrToPtr – эта опция применяется в случае, если указатель ссылается на ещё один указатель, который в свою очередь ссылается на строку текста.
Отлично, продолжаем разбираться с нашим проектом: теперь жмём «добавить блок пойнтеров» и вводим смещения начала и конца таблицы указателей (если ещё кто не понял, границы таблицы легко определяются визуально):
hEA4E – hEA86 (мы указываем адреса первых байт указателя). Жмём ОК и, если вы всё делали правильно, случится чудо! Упакованный скрипт предстанет перед вами во всей красе (кстати, вместе с байтом атрибута). Если нет – сравни файл проекта с моим – может быть, что-нибудь упустил. Если и тут всё верно – как пить дать, намудрил с плагином. Полюбовавшись скриптом и отметив, что все буквы стоят на своих местах и везде имеются байты разрыва и окончания строки, будем двигаться дальше.
Упаковка.
С тем плагином, что мы написали, пытаться вставить скрипт обратно не имеет никакого смысла – игра его не воспримет (может быть, даже не влезет в отведенное место, которое мы и так взяли с запасом). После подключения к проекту Круптара нашего плагина весь выходной поток, возвращаемый в РОМ, будет проходить через функцию «GetData», за один проход которой будет обработано одно сообщение (или в общем случае, блок, на который ссылается указатель, введённый в проект Круптара):
|
Function GetData(TextStrings: PTextStrings): String; stdcall;
//Все типы переменных описаны при разборе GetString Var R: PTextString;// Методы этого класса (см. Needs.pas): // Add - добавляет новый элемент // Root - возвращает пойнтер на первый
элемент // Cur – возвращает пойнтер на
текущий добавленный - то есть на последний begin Result := ''; If TextStrings = NIL then Exit; //Если строк нет, выходим With TextStrings^ do begin R := Root; // R - первый элемент массива While R <> NIL do begin Result :=
Result + R^.Str + #0; //Возвращаем null-terminted string R := R^.Next; // R= следующий элемент массива end; end; end; |
Думаю, пояснять что-либо излишне: по уже сложившемуся алгоритму модифицируем эту функцию следующим образом:
|
Function GetData(TextStrings: PTextStrings):
String; stdcall; Var R: PTextString; I: integer; // Счётчик прочитанных букв для контроля не вышли ли
мы за границы сообщения. Bit, DB, DI, SB, SI: byte; // Полная аналогия с
предыдущими функциями begin Result := ''; DB:=0; SB:=0; DI:=0; SI := 0; Bit:=0; I := 1; If TextStrings = NIL then Exit; With TextStrings^ do begin R := Root; With R^ do Result := Result + Char(Str[I]); //1-ый байт не упаковываем - атрибут. While R
<> NIL do begin //-------------------------Моя
процедура-------------------- With R^ do begin inc(I); SB := Byte(Str[I]); SB := SB shl 2;
//Старшие два бита кода буквы не используются. Repeat
//бесконечный цикл, выход из которого возможен только по Break. Bit := (SB and $80) shr 7; //берём старший бит кода буквы. SB := SB shl 1; inc (SI); If SI = 6 then begin
//Кончился текущий
байт. inc (I); if I >
Length(Str) then Begin //Кончился
весь входной поток. DB := DB or
Bit;
//(закидываем в выходной (упакованный) поток последний бит Result := Result + Char(DB shl (7-DI));//и добиваем нулями
последний байт выходного потока). break; end; SB := Byte(Str[I]); //Читаем код
очередной буквы из скрипта Круптара. SB := SB shl 2;
//Старшие два бита кода
буквы не используются. SI:=0; end; DB := DB or
Bit;
//Записываем наш бит в младший бит выходного потока. If DI = 7 then begin //Если байт
выходного потока записан полностью, сохраняем его. DI :=0; Result :=
Result + Char(DB); DB := 0; end else begin //Если
записан не полностю, продолжаем процедуру упаковки. DB := DB shl 1; inc (DI); end; until false end; //для with //------------------------Конец
моей процедуры---------------- R := R^.Next; end; end; end; |
Получилось несколько больше, чем функция распаковки, зато теперь мы сможем смело вставлять скрипт назад. Компилируем получившийся плагин (уже с двумя модифицированными функциями) и заменяем им тот, который был раньше в папке \Lib (имя плагину менять не стоит – в проекте Круптара уже прописано старое, а прописать новый плагин возможно только, если все группы проект пусты). Снова открываем наш проект в Круптаре и на этот раз смело жмём Ctrl+F9, пока не изменяя содержимого скрипта.
Как проверить – всё ли правильно мы упаковали? Теоретически, оригинальный РОМ и тот, куда мы только что вставили скрипт, должны быть абсолютно одинаковыми. Нам нужно будет сравнить по содержимому эти два файла. Я пользуюсь Сравнением по Содержимому в Total Commander: выделяем оба РОМа и жмём Shift+F1. Что за чёрт?! Почему появились различия? Без паники. В первую очередь, становится ясно, что различия есть и в таблицах указателей и во вставленных текстовых блоках, но различия эти явно сформированы в блоки – это видно невооружённым глазом. А произошло следующее. В оригинальной игре структура указателей и текстовых блоков была схематично такой:

В этом легко убедиться: величины указателей в таблице не всегда возрастают (оставим топорную работу с текстом на совести разработчиков). Круптар вынимает скрипт из РОМа, естественно, по указателям: текстовые блоки в проекте идут по порядку появления указателей в таблице (то есть с точки зрения проекта Круптара сначала идёт 1, потом 3, 4 и 2 текстовые блоки), а вот вставляет он скрипт обратно, ориентируясь уже только на текстовые блоки (что логично, ведь после перевода они могли измениться в размерах), а уже потом корректирует указатели под полученную структуру блоков:

Вот что мы имеем в выходном файле, но с точки зрения игры ничего не изменилось: скажем, номер сообщения равен двум: как в оригинальном случае, так и после обработки скрипта Круптаром, игра загружает второй по счету указатель (на третий текстовый блок) и начинает читать из третьего текстового блока. Поэтому мы не только не навредили, но даже привели в порядок блоки сообщений в игре! Все эти теоретические догадки подтверждаются элементарным тестированием. Вот первый самовольно перемещённый Круптаром текстовый блок (в скрипте он шестой по счёту):

Всё работает правильно и появляется в нужном месте. Как видите, я даже немного поменял текст, чтобы убедиться в правильности упаковки.
Остался у нас один должок – это я про то, что мы не знали
где конец последнего упакованного блока, а значит, не могли точно определить
границы блоков для текста, помнишь? Самое время сделать это. Дело в том, что
Круптар после упаковки текста в указанное нами место («блоки для текста»)
заполняет оставшиеся до конца зарезервированного блока байты нулями. Соответственно,
так как пакует наш плагин точно так же, как и игра, начало большого блока с
нулями в изменённом РОМе будет свидетельствовать о конце упакованного текста. В
инструменте сравнения по содержимому нужно отыскать нули в изменённом РОМе и
посмотреть, где они начинаются: в нашем случае это смещение 0xF0DC (кстати, сразу после него начинается блок с какими-то
указателями). В проекте вбиваем этот адрес как конечный (не забываем
восстановить переводимый РОМ – он уже запорот), и получаем абсолютно корректный
скрипт, с которым можно спокойно работать переводчику.
Конечно, с непривычки может показаться, что всё это дело очень сложное. Однако поверьте мне на слово: без всего этого можно провозиться раз в пять дольше. Помнится, когда я писал всё по старинке – распаковщик вышел через три дня, а упаковщик – ещё дня через два (я, правда, до сих пор не могу понять, как он работал). На написание всего этого документа, вместе с изучением алгоритма и вознёй с картинками у меня ушёл ровно один день.